/*
 * Copyright 2008 Andrej Herich
 * 
 * http://azetclient.yw.sk/
 * http://azetclient.googlecode.com/
 * 
 * This file is part of AzetClient.
 * 
 * AzetClient is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * AzetClient is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with AzetClient.  If not, see <http://www.gnu.org/licenses/>.
 */

package sk.yw.azetclient.storage;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.Deque;
import java.util.LinkedList;

/**
 *
 * @author error216
 */
public class IndentingXMLWriter extends AbstractXMLWriter {
    
    public static final String DEFAULT_INDENT_STRING = "    ";
    public static final String LINE_SEPARATOR =
            System.getProperty("line.separator");
    
    private BufferedWriter writer;
    private String version;
    private String encoding;
    private String indentString = DEFAULT_INDENT_STRING;
    private int indentLevel = 0;
    
    private Deque<String> qNames = new LinkedList<String>();
    private boolean elementEmpty = false;
    
    public IndentingXMLWriter(OutputStream out, String version,
            String encoding) {
        if (out == null)
            throw new IllegalArgumentException("Null pointer in out");
        
        try {
            this.writer = new BufferedWriter(new OutputStreamWriter(out,
                    encoding));
        } catch (UnsupportedEncodingException ex) {
            throw new IllegalArgumentException("Unsupported encoding", ex);
        }
        this.version = version;
        this.encoding = encoding;
    }

    public String getIndentString() {
        return indentString;
    }

    public void setIndentString(String indentString) {
        this.indentString = indentString;
    }
    
    private void writeIndent() throws IOException {
        for (int i = 0; i < indentLevel; i++) {
            writer.write(indentString);
        }
    }
    
    public void startDocument() throws XMLWriterException {
        try {
            writeIndent();
            writer.write("<?xml version=\"");
            writer.write(version);
            writer.write("\" encoding=\"");
            writer.write(encoding);
            writer.write("\"?>");
            if (indentString != null) writer.write(LINE_SEPARATOR);
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to write to stream", ex);
        }
    }
    
    @Override
    public void endDocument() {
        
    }
    
    public void startElement(String prefix, String localName)
            throws XMLWriterException {
        try {
            if (elementEmpty) {
                writer.write(">");
                writer.write(LINE_SEPARATOR);
            }
            writeIndent();
            String qName = (prefix != null) ? (prefix  + ":" + localName) : localName;
            qNames.push(qName);
            writer.write('<');
            writer.write(qName);
            
            elementEmpty = true;
            indentLevel++;
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to write to stream", ex);
        }
    }

    public void endElement() throws XMLWriterException {
        if (qNames.isEmpty())
            throw new IllegalStateException("No open element");
        
        try {
            indentLevel--;
            if (elementEmpty) {
                writer.write(" />");
                qNames.pop();
                elementEmpty = false;
            } else {
                writeIndent();
                writer.write("</");
                writer.write(qNames.pop());
                writer.write('>');
            }
            writer.write(LINE_SEPARATOR);
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to write to stream", ex);
        }
    }

    public void writeAttribute(String prefix, String localName, String value)
            throws XMLWriterException {
        if (!elementEmpty)
            throw new IllegalStateException("No empty element");
        
        try {
            writer.write(' ');
            if (prefix != null) {
                writer.write(prefix);
                writer.write(':');
            }
            writer.write(localName);
            writer.write("=\"");
            writer.write(escape(value));
            writer.write("\"");
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to write to stream", ex);
        }
    }

    public void startPrefixMapping(String prefix, String uri)
            throws XMLWriterException {
        if (!elementEmpty)
            throw new IllegalStateException("No empty element");
        
        try {
            writer.write(" xmlns");
            if (prefix != null) {
                writer.write(':');
                writer.write(prefix);
            }
            writer.write("=\"");
            writer.write(uri);
            writer.write("\"");
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to write to stream", ex);
        }
    }

    public void endPrefixMapping(String prefix) throws XMLWriterException {
    }

    public void characters(String string) throws XMLWriterException {
        try {
            if (elementEmpty) {
                writer.write(">");
                writer.write(LINE_SEPARATOR);
                elementEmpty = false;
            }
            String[] lines = escape(string).split(LINE_SEPARATOR, -1);
            for (String line : lines) {
                writeIndent();
                writer.write(line);
                writer.write(LINE_SEPARATOR);
            }
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to write to stream", ex);
        }
    }
    
    public void flush() throws XMLWriterException {
        try {
            writer.flush();
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to flush stream", ex);
        }
    }
    
    public void close() throws XMLWriterException {
        try {
            writer.close();
        } catch (IOException ex) {
            throw new XMLWriterException("Unable to close stream", ex);
        }
    }
    
    private String escape(String string) {
        StringBuilder builder = new StringBuilder(string);
        builder = replaceAll(builder, "&", "&amp;");
        builder = replaceAll(builder, "<", "&lt;");
        builder = replaceAll(builder, ">", "&qt;");
        builder = replaceAll(builder, "\"", "&quot;");
        builder = replaceAll(builder, "'", "&apos;");
        
        return builder.toString();
    }
    
    private StringBuilder replaceAll(StringBuilder builder, String target,
            String replacement) {
        int index = 0;
        while ((index = builder.indexOf(target, index)) > -1) {
            builder.replace(index, index + target.length(), replacement);
        }
        
        return builder;
    }

}
